﻿using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Runtime.Serialization;
using System.Linq;
using SmartDatingService;

namespace SmartDatingService {
    public partial class ServicePrototype {
        ///////////////////////////////////////////////////////////////////////
        // Inner Classes
        ///////////////////////////////////////////////////////////////////////
        private class ByteEqualityComparer : IEqualityComparer<byte[]> {
            public bool Equals(byte[] x, byte[] y) {
                return Convert.ToBase64String(x).Equals(
                    Convert.ToBase64String(y));
            }

            public int GetHashCode(byte[] obj) {
                return Convert.ToBase64String(obj).GetHashCode();
            }
        }

        ///////////////////////////////////////////////////////////////////////
        // Private Data
        ///////////////////////////////////////////////////////////////////////
        // Maps session key hash to user e-mail
        [DataMember]
        private static Dictionary<byte[], string> _activeUserEmails =
            new Dictionary<byte[], string>(new ByteEqualityComparer());
        // Maps session key hash to session key
        [DataMember]
        private static Dictionary<byte[], byte[]> _activeSessionKeys =
            new Dictionary<byte[], byte[]>(new ByteEqualityComparer());
        // Authenticated users (their session key hashes)
        [DataMember]
        private static List<string> _authenticated = new List<string>();
        // Lock on the session data: _activeUserEmails, _activeSessionKeys, _authenticated
        private static object _sessionDataLock = new object();

        ///////////////////////////////////////////////////////////////////////
        // Private Methods
        ///////////////////////////////////////////////////////////////////////
        private byte[] b(string s) {
            return Convert.FromBase64String(s);
        }
        private string s(byte[] b) {
            return Convert.ToBase64String(b);
        }

        private bool findSessionKey(string sessionKeyHash) {
            return _activeUserEmails.ContainsKey(b(sessionKeyHash));
        }

        private bool checkPassword(string sessionKeyHash, string encryptedPassword) {
            string email = _activeUserEmails[b(sessionKeyHash)];
            // get session key for the user
            byte[] sessionKey = sessionKeyByHash(sessionKeyHash);
            // reject not-created users
            if (email == "*") return false;

            byte[] passwordBytes = CryptoImpl.decryptAes(b(encryptedPassword), sessionKey);
            byte[] passwordHash = CryptoImpl.sha1(passwordBytes);

            return hasUserWithPasswordHash(email, s(passwordHash));
        }

        private IEnumerable<string> getHashForEmail(string email) {
            List<string> hashForMail;
            using (Entities contextData = new Entities()) {
                hashForMail =
                    (from m in contextData.Memberships
                    where m.email == email
                    select m.password_hash).ToList();
            }
            return hashForMail;
        }

        private bool hasUserWithPasswordHash(string email, string passwordHash) {
            IEnumerable<string> hashes = getHashForEmail(email);
            return (hashes.Count() > 0) &&
                (hashes.First() == passwordHash);
        }

        private OperationStatus checkSessionKey(string sessionKeyHash) {
            if (sessionKeyHash == null)
                return OperationStatus.InvalidSessionKey;

            if (!_authenticated.Contains(sessionKeyHash))
                return OperationStatus.NotAuthenticated;

            if (!findSessionKey(sessionKeyHash))
                return OperationStatus.InvalidSessionKey;

            string email = _activeUserEmails[b(sessionKeyHash)];
            if (!_activeSessionKeys.ContainsKey(b(sessionKeyHash))) {
                return OperationStatus.NotAuthenticated;
            }

            return OperationStatus.Success;
        }

        private bool userExists(string clientEmail) {
            return getHashForEmail(clientEmail).Count() > 0;
        }

        private void endSession(string sessionKeyHash) {
            _activeSessionKeys.Remove(b(sessionKeyHash));
            _activeUserEmails.Remove(b(sessionKeyHash));

            // kill all search descriptors associated with this session key hash
            IEnumerable<Guid> idsToDelete =
            from search in _searches.Values
            where Convert.ToBase64String(search.SessionKeyHash) == sessionKeyHash
            select search.ID;

            foreach (Guid id in idsToDelete) {
                _searches.Remove(id);
            }
        }


        private byte[] sessionKeyByHash(string sessionKeyHash) {
            return _activeSessionKeys[b(sessionKeyHash)];
        }

        private Guid userIdByHash(Entities contextEntities, string sessionKeyHash) {
            string clientEmail = _activeUserEmails[b(sessionKeyHash)];
            return (from u in contextEntities.Memberships
                    where u.email == clientEmail
                    select u.usr_id).First();
        }

        ///////////////////////////////////////////////////////////////////////
        // Public Methods
        ///////////////////////////////////////////////////////////////////////
        public OperationStatus KeyExchange(string clientEmail, string encryptedSessionKey) {
            // check that user with such email exists
            if ((clientEmail != "*") && !userExists(clientEmail))
                return OperationStatus.InvalidEmail;

            byte[] sessionKey = CryptoImpl.extractKeyFromPKCS1Exchange(b(encryptedSessionKey));

            // store two maps: EMAIL -> FULL KEY, KEY HASH -> EMAIL
            byte[] sessionKeyHash = CryptoImpl.sha1(sessionKey);
            lock (_sessionDataLock) {
                _activeSessionKeys[sessionKeyHash] = sessionKey;
                _activeUserEmails[sessionKeyHash] = clientEmail;
            }

            return OperationStatus.Success;
        }

        public OperationStatus Authenticate(string sessionKeyHash, string encryptedPassword) {
            lock (_sessionDataLock) {
                // Check whether session key hash and password are correct
                if (!findSessionKey(sessionKeyHash))
                    return OperationStatus.InvalidSessionKey;
                if (!checkPassword(sessionKeyHash, encryptedPassword))
                    return OperationStatus.InvalidEmailOrPassword;

                // User with this session key hash has successfully authenticated
                _authenticated.Add(sessionKeyHash);
            }

            return OperationStatus.Success;
        }

        public OperationStatus Logout(string sessionKeyHash) {
            lock (_sessionDataLock) {
                OperationStatus skStatus = checkSessionKey(sessionKeyHash);
                if (skStatus != OperationStatus.Success) return skStatus;

                endSession(sessionKeyHash);
                _authenticated.Remove(sessionKeyHash);
            }

            return OperationStatus.Success;
        }

        public OperationStatus Register(string sessionKeyHash, string clientEmail, string encryptedPassword) {
            byte[] sessionKey;
            lock (_sessionDataLock) {
                // Check whether session key hash and password are correct
                if (!findSessionKey(sessionKeyHash))
                    return OperationStatus.InvalidSessionKey;
                // Get session key to decrypt the password
                sessionKey = sessionKeyByHash(sessionKeyHash);
                // Kill the session key entry
                endSession(sessionKeyHash);
            }

            // there already is a user with such e-mail
            if (userExists(clientEmail)) {
                return OperationStatus.AccountAlreadyExists;
            }

            Entities contextData = new Entities();
            try {
                byte[] passwordBytes = CryptoImpl.decryptAes(b(encryptedPassword), sessionKey);
                byte[] passwordHash = CryptoImpl.sha1(passwordBytes);
                contextData.Memberships.AddObject(new Membership() {
                    usr_id = Guid.NewGuid(),
                    email = clientEmail,
                    password_hash = Convert.ToBase64String(passwordHash)
                });
                contextData.SaveChanges();
            } catch (System.Data.OptimisticConcurrencyException) {
                return OperationStatus.AccountAlreadyExists;
            } catch (Exception) {
                return OperationStatus.Failure;
            } finally {
                contextData.Dispose();
            }

            return OperationStatus.Success;
        }

        public OperationStatus Unregister(string sessionKeyHash, string clientEmail) {
            byte[] sessionKey;
            lock (_sessionDataLock) {
                // Check whether session key hash and password are correct
                OperationStatus skCheck = checkSessionKey(sessionKeyHash);
                if (skCheck != OperationStatus.Success) return skCheck;
                // Get session key to decrypt the password
                sessionKey = sessionKeyByHash(sessionKeyHash);
                // Kill the session key entry
                endSession(sessionKeyHash);
            }

            if (!userExists(clientEmail))
                return OperationStatus.InvalidEmail;

            Entities contextEntities = new Entities();
            try {
                Membership userToDelete = (from m in contextEntities.Memberships
                                           where m.email == clientEmail
                                           select m).First();
                contextEntities.Memberships.DeleteObject(userToDelete);
                contextEntities.SaveChanges();
            } catch (Exception) {
                return OperationStatus.Failure;
            } finally {
                contextEntities.Dispose();
            }

            return OperationStatus.Success;
        }
    }
}